良好程式碼的優點大同小異。
不好的程式碼的糙點卻各有巧妙之處。
Linus Torvalds 生生 在 TED 的分享中[1] 提到了對於程式碼品味的事。在此節錄一段較關鍵的內容
It does not have the if statement. And it doesn't really matter -- I don't want you understand why it doesn't have the if statement, but I want you to understand that sometimes you can see a problem in a different way and rewrite it so that a special case goes away and becomes the normal case. And that's good code. But this is simple code. This is CS 101. This is not important -- although, details are important.[name=Linus, 15:34, TED]
簡單的說,if 是「糙點發生的出發點」。
好的工程師,會懂得到很多個需求中,找出一致的邏輯。
但是,這完全取決於「整體概念性」,而不只整理語法的結果。
用一個物件導向的例子來說明吧。
這是一個偵測器,量測 Lv
值,所以只有 get 各種的值
const sensor = new Sensor("simulation");
const Lv = sensor.Lv();
const x = sensor.x();
const y = sensor.y();
但是,這看似平靜的使用方式,其實每一個實作裡充滿的糙 code 呀!!
class Sensor {
constructor (mode) {
this.mode = mode;
}
get Lv () {
if (mode == 'simulation')
return Math.random() * 100;
else
return this._Lv;
}
get x () {
if (mode == 'simulation')
return Math.random() * 10;
else
return this._x;
}
get y () {
if (mode == 'simulation')
return Math.random() * 10;
else
return this._y;
}
}
看到了滿滿的 if
了嗎?
是不是會有漏寫的?寫反寫錯,如果模式很多是不是會亂掉,不照順序寫會不會更糙?
如何降低糙點?
建立一個 simSensor 和 realSensor 並且繼承 Sensor 這個基礎類別。
class simSensor extends Sensor {
get Lv () { return Math.random() * 100; }
get x () { return Math.random() * 10; }
get y () { return Math.random() * 10; }
}
class realSensor extends Sensor {
get Lv () { return super._Lv; }
get x () { return super._x; }
get y () { return super._y; }
}
使用上只有改變一點點
// const sensor = new Sensor("simulation");
// 改成下面這樣寫
const sensor = new simSensor();
// const sensor = new realSensor();
const Lv = sensor.Lv();
const x = sensor.x();
const y = sensor.y();
這樣是不是維護起來舒適多了呢?
在處理 Array 元素重複問題時,常常會寫 if 並且走訪元素找重複。
好一點的,會使用 Array.prototype.includes
var arr = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
var arr2 = []
arr.forEach(item => {
if (!arr2.includes(item)) {
arr2.push(item)
}
})
arr2 // [1, 2, 3, 4, 5]
但是,這很糙!!!
其實,只要換容器,就可以輕鬆做到這件事。
var arr = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
var arr2 = [...(new Set(arr))] // [1, 2, 3, 4, 5]
你看!沒有任何的 if
這樣的品味才對
在看下一段的解釋之前,先看看這一段 code
// origList = []
query = origList.filter(item =>
(id.isChecked && item.id.includes(id.text)) &&
(name.isChecked && !!item.name && item.name.includes(name.text) ||
!!item.englishName && item.englishName.includes(name.text)).split("")
);
偷瞄到了嗎?看不懂再移動視線到上面看看這段 code 吧!
解釋解釋這段在寫什麼鬼條件。
它原本不是 js 的程式,為了避免侵權的問題,在此改寫成 js 的 code。
不影響它的糙點!
這是一個,畫面要做 filter 的例子。
使用方式:
發現,這麼複雜的邏輯一次曝露出太多的「程式細節」了。
有時,我們只是想知道,它考慮某個條件的邏輯是如何如何,或者想知道有沒有考慮某個條件。
如何降低糙點?
只要能夠用一個名稱來稱呼這些邏輯,就可以用具名的 function 包起來。
function filterId (item) {
return item.id.includes(id.text)
}
function filterName (item) {
return !!item.name && item.name.includes(name.text)
}
function filterEnglistName (item) {
return !!item.englishName && item.englishName.includes(name.text)
}
// origList = []
query = origList.filter(item =>
id.isChecked && filterId(item) &&
name.isChecked && filterName(item) || filterEnglistName(item)).split("")
);
在此之前,很難發現,它把 name 和 englishName 當作相同的欄位一起 filter 對吧?
現在,發現了這些檢查的邏輯其實很類似,可以讓它們合併合併。
並且,可以相容 (或修正) id 欄位的檢查邏輯
function isKeyword (word, inputWord) {
return !!word && word.includes(inputWord)
}
// origList = []
query = origList.filter(item =>
id.isChecked && isKeyword(item.id, id.text) &&
name.isChecked && isKeyword(item.name, name.text) || isKeyword(item.englishName, name.text)).split("")
);
這樣也可以跟別人說「所有欄位的檢查邏輯是不是相同的」。
如果這段用在 if
if 裡面會常出現複雜的判斷邏輯,我們把剛剛的 filter 改寫改寫。
// 原本的寫法
if ((id.isChecked && item.id.includes(id.text)) &&
(name.isChecked && !!item.name && item.name.includes(name.text) ||
!!item.englishName && item.englishName.includes(name.text)).split("")) {
return item;
}
這麼一坨,真的是一坨呀!!! (和剛剛一樣的東西)
寫這種東西是你的驕傲嗎?
改成這樣
if (id.isChecked && isKeyword(item.id, id.text) &&
name.isChecked && isKeyword(item.name, name.text) || isKeyword(item.englishName, name.text)) {
return item;
}
看上去的 DX[2] 是不是就舒適多了?
意思是
寫成 JavaScript 的話,就是
且 = AND
或 = OR
!(p && q) === (!p || !q)
!(p || q) === (!p && !q)
邏輯加上 NOT
p -> !p
q -> !q
&& -> ||
||-> &&
所以,遇到 if ()
裡的邏輯很複雜時,not
加得很亂時,就可以考慮使用 De Morgan’s laws 整理,也許可以換個角度思考或理解,也可以在邏輯上達到等價[4]。
以剛剛的例子,加上 not
就變這樣
// 原本的寫法
if ((!id.isChecked || !item.id.includes(id.text)) ||
(!name.isChecked || !item.name || !item.name.includes(name.text) &&
!item.englishName || !item.englishName.includes(name.text)).split("")) {
// nothing
}
else {
return item;
}
var a = 3.2
if (a + 0.1 === 3.3) {
//...
}
a + 0.1 === 3.3
判斷的結果是 false
console.log(a + 0.1)
// 3.3000000000000003
有沒有想過,你的判斷試會正確,完全是碰巧的 (走運)。
var a = 3.2;
var b = a + 0.1;
if (a + 0.1 === b) {
//...
}
其實是 3.3000000000000003 === 3.3000000000000003
呀!!
float
最早是由 IEEE 754[5] 來的,之後也許有其它更進步的標準,但是還是可以透過它來了解「浮點誤差」。簡單的說 float
是一種 科學記號的儲存方式[6] 造成的誤差現象。
所以,float
在 if
裡,就不能直接使用 ==
或 ===
這類「偵測相等」的判斷方式。
必須要用精度夾擊(寫到這一刻想到的詞)。
若想偵測的精度是小數點以下一位
var a = 3.2
if (a + 0.1 > 3.29 && a + 0.1 < 3.31) {
//...
}
a + 0.1 > 3.29 && a + 0.1 < 3.31
判斷的結果是 true
而且,這樣寫符合精度。
[1]: Linus Torvalds: The mind behind Linux | TED Talk
[2]: 工程師心中最軟的一塊:談前端開發者體驗(Developer Experience)
[3]: De Morgan's laws
[4]: 邏輯等價 - 維基百科,自由的百科全書
[5]: IEEE 754 - 維基百科,自由的百科全書
[6]: 浮點數 - 維基百科,自由的百科全書